// Description: 因为官方go-zero框架内的进程内缓存不支持多个失效时间的实现，所以为了满足本应用的业务场景，做了重写
package xcache

import (
	"container/list"
	"errors"
	"sync"
	"sync/atomic"
	"time"

	"github.com/zeromicro/go-zero/core/collection"
	"github.com/zeromicro/go-zero/core/logx"
	"github.com/zeromicro/go-zero/core/mathx"
	"github.com/zeromicro/go-zero/core/stores/cache"
	"github.com/zeromicro/go-zero/core/syncx"
)

const (
	defaultCacheName = "xcache"
	statInterval     = time.Minute
	slots            = 300
	// 使过期不稳定以避免大量缓存项同时过期
	// 使不稳定的到期时间为 [0.95, 1.05] 秒
	expiryDeviation = 0.05
)

var (
	ErrNotFound           = errors.New("not found")
	sharedCalls           = syncx.NewSingleFlight()
	stats                 = cache.NewStat("xcache")
	defaultExpiry         = time.Minute * 5  // 默认缓存时间
	defaultNotFoundExpiry = time.Second * 10 // 没有数据默认缓存结果时间
	gCache                = newCache(WithLimit(5000), WithName(defaultCacheName))
)

type (
	lru interface {
		add(key string)
		remove(key string)
	}

	emptyLru struct{}

	keyLru struct {
		limit    int
		evicts   *list.List
		elements map[string]*list.Element
		onEvict  func(key string)
	}

	cacheStat struct {
		name         string
		hit          uint64
		miss         uint64
		sizeCallback func() int
	}
)

func (elru emptyLru) add(string) {
}

func (elru emptyLru) remove(string) {
}

func newKeyLru(limit int, onEvict func(key string)) *keyLru {
	return &keyLru{
		limit:    limit,
		evicts:   list.New(),
		elements: make(map[string]*list.Element),
		onEvict:  onEvict,
	}
}

func (klru *keyLru) add(key string) {
	if elem, ok := klru.elements[key]; ok {
		klru.evicts.MoveToFront(elem)
		return
	}

	// 添加一个新的elem
	elem := klru.evicts.PushFront(key)
	klru.elements[key] = elem

	// 验证大小未超出
	if klru.evicts.Len() > klru.limit {
		klru.removeOldest()
	}
}

func (klru *keyLru) remove(key string) {
	if elem, ok := klru.elements[key]; ok {
		klru.removeElement(elem)
	}
}

func (klru *keyLru) removeOldest() {
	elem := klru.evicts.Back()
	if elem != nil {
		klru.removeElement(elem)
	}
}

func (klru *keyLru) removeElement(e *list.Element) {
	klru.evicts.Remove(e)
	key := e.Value.(string)
	delete(klru.elements, key)
	klru.onEvict(key)
}

func newCacheStat(name string, sizeCallback func() int) *cacheStat {
	st := &cacheStat{
		name:         name,
		sizeCallback: sizeCallback,
	}
	go st.statLoop()
	return st
}

func (cs *cacheStat) IncrementHit() {
	atomic.AddUint64(&cs.hit, 1)
}

func (cs *cacheStat) IncrementMiss() {
	atomic.AddUint64(&cs.miss, 1)
}

func (cs *cacheStat) statLoop() {
	ticker := time.NewTicker(statInterval)
	defer ticker.Stop()

	for range ticker.C {
		hit := atomic.SwapUint64(&cs.hit, 0)
		miss := atomic.SwapUint64(&cs.miss, 0)
		total := hit + miss
		if total == 0 {
			continue
		}
		percent := 100 * float32(hit) / float32(total)
		// 状态日志
		logx.Statf("cache(%s) - qpm: %d, hit_ratio: %.1f%%, elements: %d, hit: %d, miss: %d",
			cs.name, total, percent, cs.sizeCallback(), hit, miss)
	}
}

var emptyLruCache = emptyLru{}

type (
	// 定义了自定义缓存的方法。
	CacheOption func(cache *Cache)

	// 对象是内存中的缓存。
	Cache struct {
		name           string
		lock           sync.RWMutex
		data           map[string]interface{}
		timingWheel    *collection.TimingWheel
		lruCache       lru
		barrier        syncx.SingleFlight
		unstableExpiry mathx.Unstable
		stats          *cacheStat
	}
)

// 获取一个本地缓存直接外部使用
func NewLocalCache(limit ...int) *Cache {
	if len(limit) > 0 {
		return newCache(WithLimit(limit[0]), WithName(defaultCacheName))
	}
	return newCache(WithLimit(5000), WithName(defaultCacheName))
}

// 返回一个给定过期时间的缓存。
func newCache(opts ...CacheOption) *Cache {
	c := &Cache{
		data:           make(map[string]interface{}),
		lruCache:       emptyLruCache,
		barrier:        syncx.NewSingleFlight(),
		unstableExpiry: mathx.NewUnstable(expiryDeviation),
	}

	for _, opt := range opts {
		opt(c)
	}

	if len(c.name) == 0 {
		c.name = defaultCacheName
	}
	c.stats = newCacheStat(c.name, c.size)

	timingWheel, err := collection.NewTimingWheel(time.Second, slots, func(k, v interface{}) {
		key, ok := k.(string)
		if !ok {
			return
		}

		c.Del(key)
	})
	if err != nil {
		panic(err)
	}

	c.timingWheel = timingWheel
	return c
}

// 删除具有给定键的项目
func (c *Cache) Del(key string) {
	c.lock.Lock()
	delete(c.data, key)
	c.lruCache.remove(key)
	c.lock.Unlock()
	c.timingWheel.RemoveTimer(key)
}

// 返回具有给定键的项目
func (c *Cache) Get(key string) (interface{}, bool) {
	value, ok := c.doGet(key)
	if ok {
		c.stats.IncrementHit()
	} else {
		c.stats.IncrementMiss()
	}

	return value, ok
}

// 使用键将值设置为value,并带过期时间
func (c *Cache) Set(key string, value interface{}, exp time.Duration) {
	c.lock.Lock()
	_, ok := c.data[key]
	c.data[key] = value
	c.lruCache.add(key)
	c.lock.Unlock()

	expiry := c.unstableExpiry.AroundDuration(exp)
	if ok {
		c.timingWheel.MoveTimer(key, expiry)
	} else {
		c.timingWheel.SetTimer(key, value, expiry)
	}
}

// Take 返回具有给定键的值
// 如果值存在，则直接返回
// 如果没有，使用 fetch 方法获取该值，设置并返回它
func (c *Cache) Take(key string, fetch func() (interface{}, error), exp, notFoundExp time.Duration) (interface{}, error) {
	if val, ok := c.doGet(key); ok {
		c.stats.IncrementHit()
		return val, nil
	}

	var fresh bool
	val, err := c.barrier.Do(key, func() (interface{}, error) {
		// 在内存中进行搜索的的效率是 O(1), fetch 是一个 IO 查询，所以我们做双重检查，缓存可能被另一个调用占用
		if val, ok := c.doGet(key); ok {
			return val, nil
		}

		v, e := fetch()
		if e != nil && e != ErrNotFound {
			return nil, e
		}

		// 找不到的情况，需要单独设置缓存时间
		if e == ErrNotFound {
			c.Set(key, v, notFoundExp)
		} else {
			c.Set(key, v, exp)
		}

		fresh = true

		return v, e
	})
	if err != nil {
		return nil, err
	}

	if fresh {
		c.stats.IncrementMiss()
		return val, nil
	}

	c.stats.IncrementHit()
	// 从上一个正在进行的查询中得到结果
	return val, nil
}

func (c *Cache) doGet(key string) (interface{}, bool) {
	c.lock.RLock()
	defer c.lock.RUnlock()

	value, ok := c.data[key]
	if ok {
		c.lruCache.add(key)
	}

	return value, ok
}

func (c *Cache) Keys() []string {
	c.lock.RLock()
	defer c.lock.RUnlock()
	keys := make([]string, 0, len(c.data))
	for key := range c.data {
		keys = append(keys, key)
	}
	return keys
}

func (c *Cache) onEvict(key string) {
	delete(c.data, key)
	// 从时间轮盘删除这个key
	c.timingWheel.RemoveTimer(key)
}

func (c *Cache) size() int {
	c.lock.RLock()
	defer c.lock.RUnlock()
	return len(c.data)
}

func WithLimit(limit int) CacheOption {
	return func(cache *Cache) {
		if limit > 0 {
			cache.lruCache = newKeyLru(limit, cache.onEvict)
		}
	}
}

func WithName(name string) CacheOption {
	return func(cache *Cache) {
		cache.name = name
	}
}
